import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Scanner;

public class TextBuddy {

  private static final String CMD_SEARCH = "search";
  private static final String CMD_SORT = "sort";
  private static final String CMD_DELETE = "delete";
  private static final String CMD_CLEAR = "clear";
  private static final String CMD_ADD = "add";
  private static final String CMD_DISPLAY = "display";
  private static final String CMD_EXIT = "exit";

  private static final String MSG_SEARCH_RESULTS = "search results for %1$s:";
  private static final String MSG_SEARCH_NO_RESULTS_FOUND = "search: no results found!";
  private static final String MSG_FILE_SORTED = "File sorted.";
  private static final String MSG_NO_SUCH_COMMAND_ERROR = "No such command: %1$s!";
  private static final String MSG_LINE_DOES_NOT_EXIST_ERROR = "ERROR: Line %1$s does not exist!";
  private static final String MSG_FILE_EMPTY = "%1$s is empty";
  private static final String MSG_COMMAND_PROMPT = "command: ";
  private static final String MSG_WELCOME = "Welcome to TextBuddy. %1$s is ready for use";
  private static final String MSG_ADDED_LINE_TO_FILENAME = "added to %1$s: \"%2$s\"";
  private static final String MSG_DELETED_LINE_FROM_FILE = "deleted from %1$s: \"%2$s\"";
  private static final String MSG_REWRITE_ERROR = "Rewrite error!";
  private static final String MSG_SYNTAX_ERROR = "Syntax error. Usage: TextBuddy <filename>";
  private static final String MSG_DELETED_ALL_CONTENT = "all content deleted from %1$s";

  private static File file;
  private static String fileName;

  public static void main(String[] args) {
    if (args.length != 1) {
      System.out.println(MSG_SYNTAX_ERROR);

    } else {
      getAndProcessCommand(args);
    }

  }

  /**
   * Searches from file using array exprArr.
   * 
   * @param file
   * @param exprArr String array used for search. First element in array is
   *          ignored.
   * @return Matching file contents as an ArrayList with line number in file
   *         added.
   */
  public static ArrayList<String> search(File file, String[] exprArr) {
    ArrayList<String> searchResults = new ArrayList<String>();
    ArrayList<String> fileContents = displayFile(file);
    String expr = "";

    expr = combineTokensAndTrim(exprArr);

    findMatches(searchResults, fileContents, expr);

    return searchResults;

  }

  /**
   * Looks for matches of an expression in an ArrayList<String> and saves
   * results in searchResults
   * 
   * @param searchResults Contains matches
   * @param data Contains contents to search for
   * @param expr Search expression
   */
  private static void findMatches(ArrayList<String> searchResults,
      ArrayList<String> data, String expr) {
    for (String line : data) {
      if (line.contains(expr)) {
	searchResults.add(line);
      }
    }
  }

  /**
   * Combines an array of Strings into a single String, excludes the first
   * element and trims whitespaces.
   * 
   * @param strArr
   * @return
   */
  private static String combineTokensAndTrim(String[] strArr) {
    String expr;
    expr = combineTokens(strArr);
    expr = expr.trim();
    return expr;
  }

  /**
   * Gets command from input and processes accordingly.
   * 
   * @param args Input by user. Valid commands: exit, display, add, clear,
   *          delete. Case insensitive.
   */
  private static void getAndProcessCommand(String[] args) {
    String command;
    String inputLine;
    String[] tokens;
    Scanner sc = new Scanner(System.in);

    fileName = args[0];

    createNewFile();

    System.out.print(MSG_COMMAND_PROMPT);

    inputLine = sc.nextLine();
    tokens = inputLine.split(" ");
    command = tokens[0].toLowerCase();

    while (!command.equals(CMD_EXIT)) {
      if (command.equals(CMD_DISPLAY)) {
	printStringArrayList(displayFile(file));
      } else if (command.equals(CMD_ADD)) {
	addLineToFile(file, tokens);
      } else if (command.equals(CMD_CLEAR)) {
	clearFile(file, fileName);
      } else if (command.equals(CMD_DELETE)) {
	System.out.println(deleteLine(file, fileName, tokens[1]));
      } else if (command.equals(CMD_SORT)) {
	sort(file);
	System.out.println(MSG_FILE_SORTED);
      } else if (command.equals(CMD_SEARCH)) {
	ArrayList<String> searchResults = search(file, tokens);
	if (searchResults.size() == 0) {
	  System.out.println(MSG_SEARCH_NO_RESULTS_FOUND);
	} else {
	  System.out.println(String.format(MSG_SEARCH_RESULTS, combineTokens(tokens)));
	  for (String results : searchResults) {
	    System.out.println(results);
	  }
	}

      } else {
	System.out.println(String.format(MSG_NO_SUCH_COMMAND_ERROR, command));
      }

      System.out.print(MSG_COMMAND_PROMPT);

      inputLine = sc.nextLine();
      tokens = inputLine.split(" ");
      command = tokens[0].toLowerCase();
    }
    sc.close();
  }

  /**
   * Sorts contents in file by alphabetical order
   * 
   * @param file File to sort
   */
  public static void sort(File file) {
    ArrayList<String> fileContent = new ArrayList<String>();

    fileToArrayList(file, fileContent);
    Collections.sort(fileContent);
    arrayListToFile(file, fileContent);

  }

  /**
   * Writes an ArrayList<String> to a file
   * 
   * @param file
   * @param lines
   */
  private static void arrayListToFile(File file, ArrayList<String> lines) {
    try {
      BufferedWriter writer = new BufferedWriter(new FileWriter(file));
      for (String line : lines) {
	writer.write(line);
	writer.newLine();
      }
      writer.close();
    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Copies file contents to an array list
   * 
   * @param file file to read from
   * @param results ArrayList to write to
   */
  private static void fileToArrayList(File file, ArrayList<String> results) {
    try {
      String nextLine;
      BufferedReader reader = new BufferedReader(new FileReader(file));

      while ((nextLine = reader.readLine()) != null) {
	results.add(nextLine);
      }

      reader.close();
    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Deletes a line in the file.
   * 
   * @param file
   * @param fileName
   * @param lineToDel
   * @return
   */
  public static String deleteLine(File file, String fileName, String lineToDel) {
    try {
      File tmpFile = new File(fileName + ".tmp");
      int lineToDelInt = Integer.valueOf(lineToDel);

      String deletedLine = "";

      BufferedReader bufferedReader = new BufferedReader(new FileReader(file));
      BufferedWriter bufferedWriter = new BufferedWriter(new FileWriter(tmpFile));

      deletedLine = fileDeleteLine(lineToDelInt, bufferedReader, bufferedWriter);

      closeFileReaders(bufferedReader, bufferedWriter);

      boolean isRenamed = tmpFile.renameTo(file);

      if (!isRenamed) {
	return MSG_REWRITE_ERROR;
      } else if (deletedLine.equals("")) {
	return String.format(MSG_LINE_DOES_NOT_EXIST_ERROR, lineToDelInt);
      } else {
	return String.format(MSG_DELETED_LINE_FROM_FILE, fileName, deletedLine);
      }

    } catch (IOException ex) {
      return "IO exception error";
    }
  }

  /**
   * Reads file in bufferedReader and writes all content except lineToDelInt to
   * file in bufferedWriter
   * 
   * @param lineToDelInt line number to delete
   * @param deletedLine deleted line in string format
   * @param bufferedReader file to read
   * @param bufferedWriter file to write
   * @return returns deletedLine in string format
   * @throws IOException
   */
  private static String fileDeleteLine(int lineToDelInt,
      BufferedReader bufferedReader, BufferedWriter bufferedWriter)
      throws IOException {
    String deletedLine = "";
    String line;
    int lineNbr = 0;
    while ((line = bufferedReader.readLine()) != null) {
      if (lineNbr != lineToDelInt) {
	bufferedWriter.write(line);
	bufferedWriter.newLine();
      } else {
	deletedLine = line;
      }
      lineNbr++;
    }
    return deletedLine;
  }

  /**
   * Closes file readers
   * 
   * @param bufferedReader
   * @param bufferedWriter
   * @throws IOException
   */
  private static void closeFileReaders(BufferedReader bufferedReader,
      BufferedWriter bufferedWriter) throws IOException {
    bufferedReader.close();
    bufferedWriter.close();
  }

  /**
   * Clears all content in the file.
   */
  public static void clearFile(File file, String fileName) {
    try {
      FileWriter fileWriter = new FileWriter(file);
      fileWriter.close();

      System.out.println(String.format(MSG_DELETED_ALL_CONTENT, fileName));

    } catch (IOException ex) {
      ex.printStackTrace();
    }
  }

  /**
   * Writes a new line to the file
   * 
   * @param tokens contains input as an array of Strings separated by white
   *          space.
   */
  public static void addLineToFile(File file, String[] tokens) {
    try {
      FileWriter fileWriter = new FileWriter(file, true);
      BufferedWriter bufferedWriter = new BufferedWriter(fileWriter);

      String lineToAdd = "";

      lineToAdd = combineTokensAndTrim(tokens);

      bufferedWriter.write(lineToAdd);
      bufferedWriter.newLine();

      bufferedWriter.close();
      fileWriter.close();

      System.out.println(String.format(MSG_ADDED_LINE_TO_FILENAME, fileName, lineToAdd));
    } catch (IOException ex) {
      ex.printStackTrace();
    }

  }

  /**
   * combines string tokens from element n=1..n with a whitespace separating
   * them and returns as string.
   * 
   * @param tokens Array of String tokens
   * @param result Resulting String of combined tokens separated by whitespace.
   * @return returns string of combined tokens from n=1..n.
   */
  public static String combineTokens(String[] tokens) {
    String result = "";

    for (int i = 1; i < tokens.length; i++) {
      result = result + tokens[i] + " ";
    }
    result = result.trim();
    return result;
  }

  /**
   * creates a new file if it doesn't exist.
   */
  private static void createNewFile() {
    fileName = fileName.trim();
    file = new File(fileName);

    try {
      file.createNewFile();

      System.out.println(String.format(MSG_WELCOME, fileName));
    } catch (IOException e) {
      e.printStackTrace();
    }
  }

  /**
   * Prints out contents in file with line numbers
   * 
   * @param fileName
   * @return
   * @throws RuntimeException
   */
  public static ArrayList<String> displayFile(File file)
      throws RuntimeException {
    ArrayList<String> fileContents = new ArrayList<String>();
    BufferedReader reader = null;
    String line = "";
    int lineNbr = 0;

    try {

      reader = new BufferedReader(new FileReader(file));

      while ((line = reader.readLine()) != null) {
	fileContents.add(lineNbr + ". " + line);
	lineNbr++;
      }
      if (lineNbr == 0) {
	fileContents.add(String.format(MSG_FILE_EMPTY, fileName));
      }

      return fileContents;

    } catch (Exception e) {
      throw new RuntimeException(e);
    } finally {
      if (file != null) {
	try {
	  reader.close();
	} catch (IOException e) {
	}

      }
    }
  }

  /**
   * Prints out elements in a String ArrayList separated by a line separator.
   * 
   * @param lines
   */
  public static void printStringArrayList(ArrayList<String> lines) {
    for (String line : lines) {
      System.out.println(line);
    }
  }
}
